﻿#! python
#! -*- coding: utf-8 -*-

__author__ = 'Anton Vakhrushev'

import wx
from lib.extra import utils


class BaseImage:
    """
    Дескриптор изображения. Содержит всю информацию об изображении,
    осуществляет кеширование и загрузку картинки.
    """
    def __init__(self, owner, data=None):
        """
        @type owner: ImageListCtrl
        @param owner: компонент-владелец картинки
        """
        self.owner = owner
        self.__data = data
        self.__cache_size = (-1, -1)
        self.__cache_img = None

    def GetData(self):
        """
        @return: Получить пользовательские данные компонента
        """
        return self.__data

    def SetData(self, new_data):
        """
        Установить пользовательские данные компонента
        """
        self.__data = new_data

    @property
    def data(self):
        return self.__data

    def LoadImage(self, w, h):
        """
        Загрузить изображение.
        Дополнительные параметры ширины и высоты можно использовать при загрузке
        изображения, однако следует учесть, что изменение размера происходит в методе Load.
        Этот метод нужно переопределять в классах-наследниках.
        @param w: ширина миниатюры
        @param h: высота миниатюры
        @rtype: wx.Image
        @return: загруженное изображение
        """
        return wx.EmptyImage(w, h, True)

    def Load(self):
        """
        Загрузить изображение и изменить его размер до размера миниатюры.
        Для отображения миниатюры, нужно сперва явно вызвать этот метод,
        чтобы картинка загрузилась и сохранилась в кеше.
        """
        x, y, w, h = self.GetImageRect(self.owner.GetImageWidth(), self.owner.GetImageHeight())
        img = self.LoadImage(w, h)
        ww = img.GetWidth()
        hh = img.GetHeight()
        # проверить, если нужно изменить размер
        if ww != w or hh != h:
            img = utils.resize(img, w, h)
        self.__cache_size = (w, h)
        self.__cache_img = img

    def GetWxImage(self, w, h):
        """
        Поучить миниатюру
        Используется внутри ImageListCtrl для получения именно того изображения, которое будет отрисовано
        @param w: ширина миниатюры
        @param h: высота миниатюры
        @rtype: wx.Image
        @return: возвращает wx.Image такого размера, что он вписывается в заданный прямоугольник W x H
        """
        if self.__cache_size != (w, h):
            return wx.EmptyImage(w, h, True)
        return self.__cache_img

    def GetFrameRect(self, width, height):
        d = 8
        return d, d, width - d * 2, height - d * 2

    def GetImageRect(self, width, height):
        d = 16
        return d, d, width - d * 2, height - d * 2

    def Draw(self, canvas, width, height, index, selected):
        """
        Нарисовать миниатюру (границу и изображение)
        @param index: номер миниатюры
        @param canvas: объект, на котором будет рисование
        @param width: ширина области рисования (с границами)
        @param height: высота области рисования (с границами)
        """
        dc = wx.MemoryDC()
        dc.SelectObject(canvas)
        dc.BeginDrawing()
        self.DrawBackground(dc, width, height, index, selected)
        self.DrawInframeBackground(dc, width, height, index, selected)
        self.DrawFrame(dc, width, height, index, selected)
        self.DrawImage(dc, width, height, index, selected)
        self.DrawBadges(dc, width, height, index, selected)
        # dc.SetPen(wx.Pen(wx.RED, 1, wx.SOLID))
        # dc.DrawRectangle(0, 0, w, h)
        dc.EndDrawing()

    def DrawBackground(self, dc, width, height, index, selected):
        dc.SetPen(wx.Pen(wx.BLACK, 0, wx.TRANSPARENT))
        dc.SetBrush(wx.Brush(self.owner.GetBackgroundColour(), wx.SOLID))
        dc.DrawRectangle(0, 0, width, height)

    def DrawInframeBackground(self, dc, width, height, index, selected):
        pass
        # dc.SetBrush(wx.RED_BRUSH)
        # dc.DrawRectangleRect(wx.Rect(*self.GetFrameRect(width, height)))

    def DrawFrame(self, dc, width, height, index, selected):
        """
        Нарисовать рамку вокруг миниатюры
        """
        if selected:
            color = self.owner.GetSelectionColour()
            thickness = 2
        else:
            color = self.owner.GetFrameColour()
            thickness = 1
        dc.SetPen(wx.Pen(color, thickness, wx.SOLID))
        dc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT))
        dc.DrawRectangleRect(wx.Rect(*self.GetFrameRect(width, height)))

    def DrawImage(self, dc, width, height, index, selected):
        """
        Нарисовать изображение миниатюры
        @param index: номер миниатюры
        @param img: миниатюра
        @param dc: объект, на котором будет рисование
        @param w: ширина области рисования (с границами)
        @param h: высота области рисования (с границами)
        """
        ix, iy, iw, ih = self.GetImageRect(width, height)
        img = self.GetWxImage(iw, ih)
        bmp = wx.BitmapFromImage(img)
        dc.DrawBitmap(bmp, width / 2 - img.GetWidth() / 2, height / 2 - img.GetHeight() / 2)

    def DrawBadges(self, dc, width, height, index, selected):
        pass


# -----------------------------------------------------------------------------

class LocalImage(BaseImage):
    """
    Локальное изображение
    - изображение, которое находится на локальном компьютере или к которому
    есть доступ по стандартному пути
    """
    def __init__(self, owner, img_path):
        BaseImage.__init__(self, owner, img_path)

    def LoadImage(self, w, h):
        return wx.Image(self.GetData())

#------------------------------------------------------------------------------#


myEVT_ILC_IMAGE_SELECTED = wx.NewEventType()
EVT_ILC_IMAGE_SELECTED = wx.PyEventBinder(myEVT_ILC_IMAGE_SELECTED, 1)


class ImageListCtrlEvent(wx.PyCommandEvent):
    def __init__(self, evtType, evtId):
        wx.PyCommandEvent.__init__(self, evtType, evtId)
        self.image = None

    def SetImage(self, image):
        self.image = image

    def GetImage(self):
        return self.image


class ImageListCtrl(wx.ScrolledWindow):
    """Компонент предназначен для отображения списка миниатюр и дальнейшей работы с ними.
    Миниатюры имеют ленивую загрузку (по требованию), работает drag-n-dpor для
    перетаскивания миниатюр за пределы компонента.

    Миниатюра может быть представлена любым источником. Чтобы создать свой класс миниатюр,
    надо наследовать новый класс от BaseImage и переопределить в нем метод LoadImage,
    который отвечает за загрузку изображения из внешнего источника.
    """

    class ImageOptions:
        def __init__(self):
            self.w = 0
            self.h = 0
            self.frame_color = wx.LIGHT_GREY
            self.selected_color = wx.BLUE

    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 twidth=120, theight=120):

        wx.ScrolledWindow.__init__(self, parent, id, pos, size)

        # Параметры изображений
        self.__img_opt = ImageListCtrl.ImageOptions()
        self.__cols = 0
        self.__rows = 0

        self.__items = []
        """@type : list of BaseImage"""
        self.__selected = []
        """@type : list of int"""

        self.SetThumbnailSize(twidth, theight)

        # Events
        self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.OnResize)
        # Mouse events
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown)
        self.Bind(wx.EVT_MOTION, self.OnMouseMove)

        # self.Bind(wx.EVT_CHAR, lambda evt: evt.Skip())

    # ------------------------------------------------------------------------
    # Image manipulations

    @property
    def count(self):
        return len(self.__items)

    def Count(self):
        return self.count

    def Clear(self):
        """
        Очистить компонент. Удаляет миниатюры и снимает выделение
        """
        self.__items = []
        self.__selected = []
        self.UpdateColsAndRows()
        self.Refresh(False)

    def GetItems(self):
        """
        @return: Список элементов компонента
        """
        return self.__items

    def GetItemsData(self):
        """
        @return: Список с данными каждого элемента компонента
        """
        return [i.GetData() for i in self.__items]

    def IsSelected(self, index):
        """
        @param index: номер элемента
        @return: Выделен ли элемент в данный момент
        """
        return index in self.__selected

    def GetSelected(self):
        """
        Список выделенных элементов
        """
        return [self.__items[i] for i in self.__selected]

    def GetSelectedData(self):
        """
        @return: Список с данными каждого выделенного элемента
        """
        return [self.__items[i].GetData() for i in self.__selected]

    def GetSelectionIndex(self):
        """
        @return: Номер первого выделенного элемента, None если ничего не выделено
        """
        return self.__selected[0] if self.__selected else None

    def SetSelectionIndex(self, index):
        """
        Установить элемент под индексом index выделенным
        """
        if 0 > index >= self.count:
            raise IndexError()
        self.__selected = [index]

    def SetSelectedIndex(self, listOfIndexes):
        """
        Установить выбранные елементы в соответствии со списком индексов
        """
        for index in listOfIndexes:
            if not (0 <= index < self.count):
                raise IndexError('SetSelectionArray index out of range')
        self.__selected = listOfIndexes

    def Append(self, image):
        """
        Добавить изображение в конец списка
        @type image: BaseImage
        """
        image.owner = self
        self.__items.append(image)
        self.UpdateColsAndRows()
        self.Refresh(False)

    def AppendImages(self, images):
        """
        Добавить список изображений в конец списка
        """
        assert isinstance(images, list)
        for img in images:
            img.ownew = self
        self.__items.extend(images)
        self.UpdateColsAndRows()
        self.Refresh(False)

    def AssignImages(self, images):
        """
        Заменить имеющийся набор миниатюр указанным списком
        @type images: iterable
        """
        self.Clear()
        self.AppendImages(images)

    def Insert(self, pos, image):
        """
        Вставить изображение в список миниатюр.
        Если указанная позиция меньше нуля, то изображение
        будет добавлено в конец списка.
        """
        image.SetOwner(self)
        if 0 <= pos < self.count:
            self.__items.insert(pos, image)
        else:
            self.__items.append(image)
        self.UpdateColsAndRows()
        self.Refresh(False)

    def Remove(self, pos):
        """
        Удалить изображение из списка
        """
        self.__items[pos].owner = None
        self.__items.pop(pos)
        self.UpdateColsAndRows()
        self.Refresh(False)

    def LoadAll(self):
        """
        Выполнить функцию Load() для всех элементов компонента
        """
        for img in self.__items:
            img.Load()
        self.Refresh()

    # ------------------------------------------------------------------------

    # Manipulations with thumbnails size

    # todo сделать более точное определение координат (включая границы)
    def GetItemIndex(self, x, y):
        """
        Получить индекс миниатюры по указанным координатам внутри компонента
        """
        x /= self.__img_opt.w
        y /= self.__img_opt.h
        index = y * self.__cols + x
        if x < 0 or x >= self.__cols:
            return -1
        if y < 0 or y >= self.__rows:
            return -1
        if index < 0 or index >= self.count:
            return -1
        return index

    def SetThumbnailSize(self, width=0, height=0):
        """
        Установить размер миниатюр и пересчитать количество строк и колонок
        """
        if width > 0:
            self.__img_opt.w = width
        if height > 0:
            self.__img_opt.h = height
        self.SetScrollRate(width / 5, height / 5)
        self.UpdateColsAndRows()

    def GetImageWidth(self):
        """
        Получить ширину миниатюры
        """
        return self.__img_opt.w

    def GetImageHeight(self):
        """
        Получить высоту миниатюры
        """
        return self.__img_opt.h

    def UpdateColsAndRows(self):
        """
        Пересчитать количество строк и столбцов
        """
        # получить ширину клиентской области компонента
        cl_width = self.GetClientSize().GetWidth()
        # вычислить количество колонок
        self.__cols = cl_width // self.__img_opt.w or 1
        # вычислить количество столбцов
        rest = len(self.__items) % self.__cols  # остаток: 1 строка
        self.__rows = len(self.__items) // self.__cols + bool(rest)
        self.SetVirtualSize((self.__cols * self.__img_opt.w, self.__rows * self.__img_opt.h))

    # ------------------------------------------------------------------------

    # Операции с цветовой палитрой

    def GetSelectionColour(self):
        return self.__img_opt.selected_color

    def SetSelectionColour(self, color):
        self.__img_opt.selected_color = color

    def GetFrameColour(self):
        return self.__img_opt.frame_color

    def SetFrameColour(self, color):
        self.__img_opt.frame_color = color

    # ------------------------------------------------------------------------

    # Отрисовка компонента

    def GetPaintRect(self):
        """Получить видимый прямоугольник окна, который нужно отрисовать"""
        size = self.GetClientSize()
        paintRect = wx.Rect(0, 0, size.GetWidth(), size.GetHeight())
        # возвращает первую видимую точку в scroll units
        paintRect.x, paintRect.y = self.GetViewStart()
        xu, yu = self.GetScrollPixelsPerUnit()
        paintRect.x *= xu
        paintRect.y *= yu
        return paintRect

    def OnPaint(self, event):
        """Метод обрабатывает событие отрисовки компонента"""
        # получить видимую область компонента,
        # на которой будет происходить рисование
        dc = wx.BufferedPaintDC(self)
        self.PrepareDC(dc)
        dc.BeginDrawing()

        dc.SetPen(wx.Pen(wx.BLACK, 0, wx.TRANSPARENT))
        dc.SetBrush(wx.Brush(self.GetBackgroundColour(), wx.SOLID))

        paintRect = self.GetPaintRect()
        dc.DrawRectangleRect(paintRect)

        for idx, img in enumerate(self.__items):
            # определяем координаты колонки и строки
            row = idx // self.__cols
            col = idx % self.__cols
            # определяем координаты миниатюры в пикселях (включая границы)
            tw = self.__img_opt.w   # ширина
            th = self.__img_opt.h   # высота
            tx = col * tw           # координата X
            ty = row * th           # координата Y
            # продолжить, если н пересекается с видимой областью
            # (а значит и рисовать ничего не надо)
            if not paintRect.Intersects(wx.Rect(tx, ty, tw, th)):
                continue
            # создадим пустое изображение-буфер, на котором будем рисовать
            # все компоненты миниатюры
            canvas = wx.EmptyBitmap(tw, th)
            img.Draw(canvas, tw, th, idx, self.IsSelected(idx))
            # self.Draw(idx, img, canvas, tw, th)
            dc.DrawBitmap(canvas, tx, ty)

        dc.EndDrawing()

    # ------------------------------------------------------------------------

    # Resizing

    def OnResize(self, event):
        # персчитать количество столбцов и строк
        self.UpdateColsAndRows()
        # обновить содержимое компонента
        self.Refresh(False)

    # ------------------------------------------------------------------------

    # Mouse events

    def OnMouseDown(self, event):
        def shift_select():
            first = self.__selected[0]
            if select < first:
                self.__selected = range(first, select - 1, -1)
            else:
                self.__selected = range(first, select + 1)

        x, y = self.CalcUnscrolledPosition(event.GetX(), event.GetY())
        select = self.GetItemIndex(x, y)

        sh = event.ShiftDown()  # нажат шифт
        ct = event.ControlDown()    # нажат контрол
        wt = select in self.__selected  # выбранный элемент уже выделен
        gen_event = False

        # ничего не выбрано
        if select < 0:
            if not (sh or ct):
                self.__selected = []
        # новый элемент
        elif not wt:
            if not self.__selected:
                self.__selected = [select]
            elif ct:
                self.__selected.append(select)
            elif sh:
                shift_select()
            else:
                self.__selected = [select]
            gen_event = True
        # элемент уже был в списке
        else:
            if ct:
                self.__selected.remove(select)
            elif sh:
                shift_select()
            else:
                self.__selected = [select]

        # запускаем событие
        if gen_event:
            evt = ImageListCtrlEvent(myEVT_ILC_IMAGE_SELECTED, self.GetId())
            evt.SetInt(select)
            evt.SetImage(self.__items[select])
            self.GetEventHandler().ProcessEvent(evt)

        # обновляем компонент без перерисовки фона
        self.Refresh(False)
        self.SetFocus()

    def OnMouseMove(self, event):
        # if event.Dragging() and len(self._selectedarray):
        #     text = ''
        #     for i in self._selectedarray:
        #         text = text + self._items[i].GetDnDText()
        #
        #     text = wx.TextDataObject(text)
        #     source = wx.DropSource(self)
        #     source.SetData(text)
        #     res = source.DoDragDrop()
        pass


# -----------------------------------------------------------------------------

def main():
    app = wx.App(redirect=False)
    top = wx.Frame(None, title="Test ImageListCtrl", size=(600, 400))
    top.ilc = ImageListCtrl(top)

    top.ilc.Append(LocalImage(top.ilc, r'D:\Multimedia\Photos\test.jpg'))
    top.ilc.Append(LocalImage(top.ilc, r'D:\Multimedia\Photos\test.jpg'))
    top.ilc.LoadAll()
    top.Show()
    app.MainLoop()

if __name__ == '__main__':
    main()